fix(analyze): make FTS index creation non-fatal during analyze#1
Closed
henry201605 wants to merge 1 commit into
Closed
fix(analyze): make FTS index creation non-fatal during analyze#1henry201605 wants to merge 1 commit into
henry201605 wants to merge 1 commit into
Conversation
When the LadybugDB FTS extension cannot be loaded (e.g. macOS 12 where libc++ lacks std::to_chars(double), or container environments without the native extension), `gitnexus analyze --embeddings` fails at 85% and aborts before embedding generation can run. This is inconsistent with the MCP server's behavior: pool-adapter.ts already treats FTS load failure as a graceful degradation (ftsLoaded = false, BM25 search degrades, other features continue). Change: wrap createSearchFTSIndexes() in try/catch so that: - FTS creation failure logs a warning instead of throwing - Embedding generation (Phase 4) proceeds normally - `--repair-fts` remains available for explicit FTS retry - BM25 keyword search degrades gracefully (same as MCP read path) Affected users: macOS 12 (Monterey), any platform where the FTS extension binary references symbols missing from the system libc++.
henry201605
pushed a commit
that referenced
this pull request
May 22, 2026
…wari#1759) (abhigyanpatwari#1775) Two related bugs surfaced in REGISTRY_PRIMARY_KOTLIN=1 forced mode: 1. `import models.getRepo` silently resolved to `models/User.kt` (the first `.kt` file inside `models/` by iteration order) when no file was named after the symbol. `findKotlinFile` returned a single directory child as a fallback, so the importer's module-scope mirror only ever picked up the first arbitrary candidate — `getUser → User` landed but `getRepo → Repo` never did, and downstream `repo.save()` resolution fell through to no edge. 2. `for (x in importedCallable())` produced no for-loop type binding when the callee's return type lived in another file, because `inferKotlinIterableElementType`'s call-expression arm consulted only the local file's `returnTypes` map. Fix: - Split `findKotlinFile` into `findKotlinExactOrSuffix` (exact / suffix match only) and `findKotlinDirectoryChild` (legacy single-child fallback). Add `findKotlinPackageFiles` returning every `.kt`/`.kts` file inside a package directory. The resolver now fans out the stripped path through `findKotlinExactOrSuffix → findKotlinPackageFiles`, returning a `readonly string[]` candidate set. The finalize pass walks each candidate and picks the one whose `localDefs` actually export the imported name — exactly the multi-target contract `FinalizeHooks.resolveImportTarget` already supports. - `inferKotlinIterableElementType` for `call_expression` now falls back to the callee's identifier text when the local return-type map has no entry. `propagateImportedReturnTypes` chain-follows `loopvar → callee → ElementType` once the imported `callee → Element` mirror lands at module scope (which now works thanks to fix #1). Verification (REGISTRY_PRIMARY_KOTLIN=1): - Forced-mode: 21 -> 18 failing of 175 (3 fewer; tests 487, 1242, 1251 in test/integration/resolvers/kotlin.test.ts now green). - Default-mode Kotlin: 175/175 unchanged. - Full resolver suite: 2216/2216 unchanged (incl. `kotlin-calls` `util.OneArg.writeAudit` regression check at line 176). - Remaining 18 failures are tracked by sibling sub-issues (abhigyanpatwari#1758, abhigyanpatwari#1760, abhigyanpatwari#1761, abhigyanpatwari#1762, abhigyanpatwari#1763). Does NOT add Kotlin to MIGRATED_LANGUAGES per parent abhigyanpatwari#1746 flip criteria. Closes abhigyanpatwari#1759. Refs abhigyanpatwari#1746. Co-authored-by: Test <test@example.com>
magyargergo
added a commit
that referenced
this pull request
May 30, 2026
abhigyanpatwari#1905) * fix(csharp): eliminate O(S·D) BindingRef OOM in namespace siblings Types declared in the C# global (default) namespace are visible from every file, so the previous per-scope augmentation materialized O(scopes × defs) BindingRefs — on large Unity solutions (tens of thousands of global types) this caused severe slowness and OOM. Route global-namespace types through a single workspace-level binding channel (workspaceFqnBindings, consulted by lookupBindingsAt) for O(D) memory. Also fix quadratic costs in the non-global path: append defs in place instead of copying (was O(D²) per bucket), pre-index the first scope per file (was O(S²·D)), and seed de-dup sets instead of repeated .some scans. Add csharp-pipeline-benchmark.test.ts (mirrors the PHP benchmark) with spread and concentrated-global-namespace scenarios to track elapsedMs, peakHeapMB, nodeCount, and edgeCount. Post-fix runs show linear scaling and stable heap. Co-authored-by: Cursor <cursoragent@cursor.com> * perf(csharp): scanner fallback for namespace siblings on the worker path Worker threads can't return tree-sitter Trees across MessageChannels, so the cross-phase tree cache is empty for worker-parsed files. The C# same-namespace pass (populateCsharpNamespaceSiblings -> extractFileStructure) then re-parsed every file with tree-sitter to find namespace / using-static nodes — effectively parsing a large solution a second time during scope resolution. Add a line-scanner fallback (extractCsharpStructureViaScanner) used only when no cached Tree is available, mirroring PHP's fix for issue abhigyanpatwari#1741. It extracts the same namespaces / usingStaticPaths the AST walk produces for the common line-anchored forms (file-scoped + block namespaces, plain / global / aliased `using static`). The AST walk stays authoritative on the sequential / warm-cache path. Micro-benchmark over 3000 synthetic files: scanner is ~188x faster than parse+walk (0.001 vs 0.251 ms/file) with identical output on the parity spot-check; real-world files are larger, so the worker-path saving is bigger. Adds csharp-namespace-extraction.test.ts (12 cases) covering all declaration forms plus negative cases (using var, plain using, comments). Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(csharp): cover global-namespace workspaceFqnBindings path + doc + using-static perf Addresses the production-readiness review of the namespace-siblings OOM fix. - Add a unit test proving global-(default-)namespace C# types route to indexes.workspaceFqnBindings (one entry per simple name) with ZERO bindingAugmentations — pinning the O(D) invariant behind the abhigyanpatwari#1871 Unity-scale OOM fix and guarding against a revert to per-scope O(scopes x defs) augmentation. (The csharp-hooks mock now supplies workspaceFqnBindings, which the global fast path reads directly.) - Correct the workspaceFqnBindings doc comment: it is shared by PHP (backslash-FQN keys) and C# (global-namespace simple-name keys); the two key formats are disjoint. - Pre-index parsedFiles by path before the `using static` member-injection loop, replacing an O(files) find-per-import with an O(1) Map lookup. Verified: tsc --noEmit clean; csharp-hooks + csharp-namespace-extraction suites pass (38 tests); prettier clean; eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(csharp): apply PR-review polish to namespace-siblings (tests, types, docs) Addresses the multi-agent code review of this PR — the concrete, defensible findings. Two items intentionally deferred (below). - namespace-siblings.ts: couple the augmentation bucket + its de-dup set into one nullable lifecycle, removing the seen!/bucketArr! non-null assertions (identical runtime, still lazy). - validate-bindings-immutability.ts: extend the dev-mode immutability validator to the third channel (workspaceFqnBindings) + a test; complete the validator test mock with workspaceFqnBindings. - walkers.ts: document that namesAtScope deliberately excludes the scope-independent workspaceFqnBindings channel (enumerating workspace names at every scope would flood per-scope callers; lookupBindingsAt still consults it when resolving a specific name). - scope-resolution-indexes.ts: reframe the workspaceFqnBindings doc to describe the key-format contract language-neutrally (examples, not language branching). - csharp-hooks.test.ts: assert workspace entries carry origin:'namespace'; add a partial-class test (same simple name, distinct nodeIds across global files → both kept); rename the stale "parses" cache-miss test to "scans". - csharp-pipeline-benchmark.test.ts: clearTimeout the Promise.race budget timer (dangling handle when the pipeline won the race). - csharp.test.ts: correct the abhigyanpatwari#1066 comment — extractFileStructure no longer re-parses on cache miss (line scanner); only emitCsharpScopeCaptures re-parses. Deferred (surfaced, not applied): (1) worker-path scanner mis-reads namespace/using-static inside block comments and verbatim/raw strings — an explicitly documented trade-off mirroring the PHP scanner; hardening it to track comment/string state is a separate decision. (2) workspaceFqnBindings is read via an `as Map` cast; a type-safe mutable handle from finalize-orchestrator is a cross-module contract change. Verified: tsc --noEmit clean; 49 unit tests pass (incl. 3 new); prettier clean; eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(csharp): harden worker-path scanner + localize workspace-map cast Addresses the two deferred PR-review findings plus the remaining test gap. #1 — Worker-path scanner false positives: the line scanner now tracks block- comment and string state across lines (advanceCsScanState), so a `namespace` / `using static` keyword at the start of a line inside a block comment, verbatim string (@"..."), or raw string literal ("""...""") is no longer mistaken for a declaration on the worker cache-miss path. It matches only at code-state line starts. 5 new scanner tests cover the block-comment / raw / verbatim cases. abhigyanpatwari#4 — workspaceFqnBindings type safety: the ReadonlyMap->Map cast is localized to one documented line, and global-namespace writes go through a new getWorkspaceBucket helper (mirroring getAugmentationBucket) rather than an inline `.set()` at the mutation site. #2 — lookupBindingsAt workspace-channel coverage: walkers-augmentations.test.ts now exercises the third (workspace) channel: workspace-only, append-after- finalized/augmented, and dedup-loses-to-finalized/augmented precedence. abhigyanpatwari#5 — OOM CI guard: the deterministic O(D) invariant (zero per-scope augmentation for global types) is already asserted by the always-on csharp-hooks unit tests added earlier; the scale/time benchmark stays appropriately opt-in (skipIf). Verified: tsc --noEmit clean; 69 unit tests (4 suites) + 210 C# integration resolver tests pass; prettier clean; eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(csharp): replace remaining O(A) .some dedup scans with seeded Sets The using-static member-injection loop and the cross-namespace import loop both de-duped via `bucketArr.some((b) => b.def.nodeId === ...)` — O(A) per item. Both now use a per-file `Map<simpleName, Set<nodeId>>`, seeded lazily from the augmentation bucket (capturing entries from earlier passes), matching the global and named-namespace paths. Same dedup semantics, O(1) amortized. Verified: tsc --noEmit clean; csharp-hooks unit (27) + C# integration resolver (210) tests pass; prettier + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
henry201605
pushed a commit
that referenced
this pull request
May 30, 2026
abhigyanpatwari#1881) (abhigyanpatwari#1908) * fix(csharp): eliminate O(S·D) BindingRef OOM in namespace siblings Types declared in the C# global (default) namespace are visible from every file, so the previous per-scope augmentation materialized O(scopes × defs) BindingRefs — on large Unity solutions (tens of thousands of global types) this caused severe slowness and OOM. Route global-namespace types through a single workspace-level binding channel (workspaceFqnBindings, consulted by lookupBindingsAt) for O(D) memory. Also fix quadratic costs in the non-global path: append defs in place instead of copying (was O(D²) per bucket), pre-index the first scope per file (was O(S²·D)), and seed de-dup sets instead of repeated .some scans. Add csharp-pipeline-benchmark.test.ts (mirrors the PHP benchmark) with spread and concentrated-global-namespace scenarios to track elapsedMs, peakHeapMB, nodeCount, and edgeCount. Post-fix runs show linear scaling and stable heap. Co-authored-by: Cursor <cursoragent@cursor.com> * perf(csharp): scanner fallback for namespace siblings on the worker path Worker threads can't return tree-sitter Trees across MessageChannels, so the cross-phase tree cache is empty for worker-parsed files. The C# same-namespace pass (populateCsharpNamespaceSiblings -> extractFileStructure) then re-parsed every file with tree-sitter to find namespace / using-static nodes — effectively parsing a large solution a second time during scope resolution. Add a line-scanner fallback (extractCsharpStructureViaScanner) used only when no cached Tree is available, mirroring PHP's fix for issue abhigyanpatwari#1741. It extracts the same namespaces / usingStaticPaths the AST walk produces for the common line-anchored forms (file-scoped + block namespaces, plain / global / aliased `using static`). The AST walk stays authoritative on the sequential / warm-cache path. Micro-benchmark over 3000 synthetic files: scanner is ~188x faster than parse+walk (0.001 vs 0.251 ms/file) with identical output on the parity spot-check; real-world files are larger, so the worker-path saving is bigger. Adds csharp-namespace-extraction.test.ts (12 cases) covering all declaration forms plus negative cases (using var, plain using, comments). Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(csharp): cover global-namespace workspaceFqnBindings path + doc + using-static perf Addresses the production-readiness review of the namespace-siblings OOM fix. - Add a unit test proving global-(default-)namespace C# types route to indexes.workspaceFqnBindings (one entry per simple name) with ZERO bindingAugmentations — pinning the O(D) invariant behind the abhigyanpatwari#1871 Unity-scale OOM fix and guarding against a revert to per-scope O(scopes x defs) augmentation. (The csharp-hooks mock now supplies workspaceFqnBindings, which the global fast path reads directly.) - Correct the workspaceFqnBindings doc comment: it is shared by PHP (backslash-FQN keys) and C# (global-namespace simple-name keys); the two key formats are disjoint. - Pre-index parsedFiles by path before the `using static` member-injection loop, replacing an O(files) find-per-import with an O(1) Map lookup. Verified: tsc --noEmit clean; csharp-hooks + csharp-namespace-extraction suites pass (38 tests); prettier clean; eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(csharp): apply PR-review polish to namespace-siblings (tests, types, docs) Addresses the multi-agent code review of this PR — the concrete, defensible findings. Two items intentionally deferred (below). - namespace-siblings.ts: couple the augmentation bucket + its de-dup set into one nullable lifecycle, removing the seen!/bucketArr! non-null assertions (identical runtime, still lazy). - validate-bindings-immutability.ts: extend the dev-mode immutability validator to the third channel (workspaceFqnBindings) + a test; complete the validator test mock with workspaceFqnBindings. - walkers.ts: document that namesAtScope deliberately excludes the scope-independent workspaceFqnBindings channel (enumerating workspace names at every scope would flood per-scope callers; lookupBindingsAt still consults it when resolving a specific name). - scope-resolution-indexes.ts: reframe the workspaceFqnBindings doc to describe the key-format contract language-neutrally (examples, not language branching). - csharp-hooks.test.ts: assert workspace entries carry origin:'namespace'; add a partial-class test (same simple name, distinct nodeIds across global files → both kept); rename the stale "parses" cache-miss test to "scans". - csharp-pipeline-benchmark.test.ts: clearTimeout the Promise.race budget timer (dangling handle when the pipeline won the race). - csharp.test.ts: correct the abhigyanpatwari#1066 comment — extractFileStructure no longer re-parses on cache miss (line scanner); only emitCsharpScopeCaptures re-parses. Deferred (surfaced, not applied): (1) worker-path scanner mis-reads namespace/using-static inside block comments and verbatim/raw strings — an explicitly documented trade-off mirroring the PHP scanner; hardening it to track comment/string state is a separate decision. (2) workspaceFqnBindings is read via an `as Map` cast; a type-safe mutable handle from finalize-orchestrator is a cross-module contract change. Verified: tsc --noEmit clean; 49 unit tests pass (incl. 3 new); prettier clean; eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(csharp): harden worker-path scanner + localize workspace-map cast Addresses the two deferred PR-review findings plus the remaining test gap. #1 — Worker-path scanner false positives: the line scanner now tracks block- comment and string state across lines (advanceCsScanState), so a `namespace` / `using static` keyword at the start of a line inside a block comment, verbatim string (@"..."), or raw string literal ("""...""") is no longer mistaken for a declaration on the worker cache-miss path. It matches only at code-state line starts. 5 new scanner tests cover the block-comment / raw / verbatim cases. abhigyanpatwari#4 — workspaceFqnBindings type safety: the ReadonlyMap->Map cast is localized to one documented line, and global-namespace writes go through a new getWorkspaceBucket helper (mirroring getAugmentationBucket) rather than an inline `.set()` at the mutation site. #2 — lookupBindingsAt workspace-channel coverage: walkers-augmentations.test.ts now exercises the third (workspace) channel: workspace-only, append-after- finalized/augmented, and dedup-loses-to-finalized/augmented precedence. abhigyanpatwari#5 — OOM CI guard: the deterministic O(D) invariant (zero per-scope augmentation for global types) is already asserted by the always-on csharp-hooks unit tests added earlier; the scale/time benchmark stays appropriately opt-in (skipIf). Verified: tsc --noEmit clean; 69 unit tests (4 suites) + 210 C# integration resolver tests pass; prettier clean; eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(csharp): replace remaining O(A) .some dedup scans with seeded Sets The using-static member-injection loop and the cross-namespace import loop both de-duped via `bucketArr.some((b) => b.def.nodeId === ...)` — O(A) per item. Both now use a per-file `Map<simpleName, Set<nodeId>>`, seeded lazily from the augmentation bucket (capturing entries from earlier passes), matching the global and named-namespace paths. Same dedup semantics, O(1) amortized. Verified: tsc --noEmit clean; csharp-hooks unit (27) + C# integration resolver (210) tests pass; prettier + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(csharp): gate suffix-fallback import resolution to declared namespaces (abhigyanpatwari#1881) C# `using` directives were resolving via an ungated suffix match, so a BCL using like `System.Threading.Tasks` matched a coincidental local `Tasks.cs` and emitted spurious IMPORTS edges. Add a declared-namespace gate that only permits suffix-fallback when the import plausibly refers to an in-repo namespace (exact, immediate-parent-declared, or ancestor-of a declared namespace anchored at an in-repo root). Both resolution legs — the legacy DAG and the registry-primary scope resolver — thread the same evidence to the gate, including the no-csproj path. Declared namespaces are collected with abhigyanpatwari#1905's comment/string-aware scanner (extractCsharpStructureViaScanner, lazily imported) instead of a regex, so `namespace` tokens in comments/strings can't seed phantom namespaces. Scan truncation or unreadable subtrees fail OPEN (gate disabled) and are logged. Stacked on abhigyanpatwari#1905 (fix/csharp-namespace-scope-oom). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(csharp): cap per-file size in namespace scan; fail open on skip (abhigyanpatwari#1881) scanCSharpProject read every .cs/.csproj in full with no size guard and issued per-directory reads with no concurrency bound, an OOM/FD-exhaustion vector on large or generated repos. Add an fs.stat size guard before each read, reusing getMaxFileSizeBytes() (the same 512KB cap the Phase-1 walker uses). An oversized or unreadable .cs now signals truncation so the abhigyanpatwari#1881 suffix-fallback gate fails OPEN rather than wrongly suppressing an import whose declaring namespace lived in the skipped file (previously a silent return left the scan looking complete). Adds a size-cap scan test. * fix(csharp): bound per-directory read concurrency in namespace scan (abhigyanpatwari#1881) The scan issued every .cs/.csproj read in a directory at once via Promise.all, so in-flight file descriptors scaled with the largest directory's file count. Issue reads in bounded windows (32, mirroring the Phase-1 filesystem-walker) via Promise.allSettled; an unexpected read/scan rejection now trips truncation (fail open) instead of rejecting the whole scan. Behavior-preserving for namespace collection (C# scope-resolution parity passes on both legs). * style(csharp): apply prettier to abhigyanpatwari#1881 files to clear quality/format gate (abhigyanpatwari#1908) Reflow hand-wrapped lines in scope-resolver.ts and the csharp integration test that prettier collapses under printWidth 100. Formatting only, no behavioral change; clears the failing quality/format CI gate. * fix(csharp): stream namespace scan so large generated files don't disable the abhigyanpatwari#1881 gate (abhigyanpatwari#1908) Code-review follow-up. The scan read each .cs fully into a string behind a 512KB size cap (the tree-sitter parse budget); a single larger generated file (*.g.cs, EF/gRPC output) tripped `truncated`, making the abhigyanpatwari#1881 suffix-fallback gate fail open repo-wide and silently undoing the fix on real repos. Stream each .cs line-by-line via createReadStream + readline into a new incremental scanner (createCsharpStructureScanner) instead of buffering the whole file. Memory is now constant regardless of file size, so the per-file size cap is dropped for the namespace line-scan and large generated files are fully collected. extractCsharpStructureViaScanner is reimplemented on the same incremental scanner (byte-identical; C# parity 2/2). collectDeclaredNamespaces returns 'ok' | 'truncated' (truncation now only from an unreadable file) and the truncation warn lists its real causes. csproj reads keep their size guard. Prior art: ripgrep/ctags/Node readline stream rather than cap for line scans; GitHub (384KB) and Sourcegraph (1MB) cap only their full-content indexes. * fix(csharp): cap .csproj read via stream, not stat-then-read, to clear CodeQL TOCTOU (abhigyanpatwari#1908) CodeQL js/file-system-race flagged the fs.stat + fs.readFile size guard in readCsprojConfig as a check-then-use filesystem race. Replace it with a length-capped createReadStream (readFileTextCapped) — same memory bound on untrusted input, no stat-then-read race, and consistent with the streamed .cs scan. Behavior is unchanged for real .csproj files (parity 2/2). * fix(csharp): keep BCL/external roots gated through scan truncation (abhigyanpatwari#1908, Codex F1) A single scan truncation (unreadable dir/file, depth/dir cap) set one repo-wide `truncated` flag that made csharpSuffixFallbackAllowed fail open for EVERY import, silently re-enabling the abhigyanpatwari#1881 BCL->local suffix matches. Add a CSHARP_EXTERNAL_ROOTS denylist (System/Microsoft/...): an external-rooted using that does not align with an in-repo declared namespace stays BLOCKED even under truncation, while genuinely local-looking usings still fail open. A repo that declares the root is allowed via the alignment escape hatch. Shared predicate, so both legs inherit it. * fix(csharp): gate the registry no-csproj direct-match path (abhigyanpatwari#1908, Codex F2) In the no-csproj branch of resolveCsharpImportTarget, resolveDirectMatch ran BEFORE the gate, so a path-aligned Legacy/System/Threading/Tasks.cs satisfied 'using System.Threading.Tasks;' even though System.* is not a declared in-repo namespace — while the legacy leg (gate-first) blocked it, so the legs were not equivalent. Run csharpSuffixFallbackAllowed first (return null on fail), then direct-match, then progressive stripping — mirroring the legacy ordering. Adds a no-csproj fixture with a deep path-aligned Tasks.cs and dual-leg integration describes (registry + forced-legacy), plus a path-aligned unit case. Parity 2/2. * fix(csharp): flag scanner-uncaptured namespaces incomplete; Unicode/@ matchers (abhigyanpatwari#1908, Codex F3) The line scanner treated its output as complete even when it missed valid C# namespace forms, so the gate failed CLOSED and over-blocked legit imports. Make CS_NAMESPACE_RE/CS_USING_STATIC_RE Unicode-aware (\p{L}\p{N} + u flag) and strip leading/segment @ so verbatim/Unicode identifiers are captured to match the AST. For forms the regex still can't capture (split across lines, not at line start, attributed), set a per-file 'incomplete' flag; collectDeclaredNamespaces returns 'truncated' for such files so the abhigyanpatwari#1881 gate fails OPEN instead of dropping the namespace. High-precision detectors + guard tests keep ordinary forms (incl. // namespace comments) from tripping incomplete. * fix(csharp): stream the .csproj RootNamespace read, no byte cap (abhigyanpatwari#1908, Codex F4) readCsprojConfig read only the first 512KB of a .csproj and, on a match-miss, couldn't tell 'no RootNamespace' from 'RootNamespace past the cap' — both synthesized a filename root. A wrong authoritative root makes imports under the real root resolve to nothing AND suppresses the fallback. Replace the capped read with a streamed early-stop search (findCsprojRootNamespace) that reads until the tag or EOF: filename fallback ONLY on genuine read-to-EOF absence; on a soft-budget cap-hit or unreadable file, OMIT the config so the no-csproj fallback stays reachable. Removes the now-unused readFileTextCapped + getMaxFileSizeBytes cap from the scan. Parity 2/2. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
henry201605
pushed a commit
that referenced
this pull request
Jun 4, 2026
…higyanpatwari#1875) * chore: extend .gitattributes for shell scripts and binary assets Append explicit `*.sh text eol=lf` and `*.bash text eol=lf` rules so shell scripts (notably anything COPYed into a Linux container) check out with LF endings on Windows hosts with `core.autocrlf=true`, regardless of the auto-detection on the existing `* text=auto eol=lf` line. Add binary markers for `*.node`, `*.wasm`, `*.onnx`, `*.so`, `*.dll`, `*.dylib` so native and ML model artifacts aren't ever subjected to text normalization. The existing `* text=auto eol=lf` and `.husky/* text eol=lf` rules are preserved. `git ls-files --eol` confirmed zero CRLF or mixed blobs in the index, so no `--renormalize` was needed. * feat(devcontainer): add cross-platform devcontainer for Claude Code, Codex, and Cursor CLIs Add a Dev Container that pre-installs Claude Code (2.1.153, via Anthropic's official Feature), OpenAI Codex CLI (pinned 0.134.0), and Cursor CLI alongside the GitNexus native build chain. Opens via VS Code's Dev Containers extension on Windows 11 (Docker Desktop + WSL2), macOS, or Linux without OS-specific branches in devcontainer.json. Topology and base - Base image `mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm` (multi-arch, monthly patched, ships the `node` non-root user, zsh, `gh`). - Node 22 LTS satisfies `gitnexus/`'s engines `>=22.0.0` and matches the `node:22-bookworm-slim` SHA-pinned base used by `Dockerfile.cli`. - Single container with all three CLIs co-installed (vs. docker-compose per-tool) — prevailing 2026 community pattern, lowest daily-driver friction. Persistence and auth - Per-devcontainer named volumes scoped by `${devcontainerId}` for `/home/node/.claude`, `/home/node/.codex`, `/home/node/.cursor`, `/commandhistory`, and `/home/node/.npm`. Authentication survives rebuilds without leaking between workspaces. - Four sub-workspace `node_modules` volumes (root, gitnexus, gitnexus-web, gitnexus-shared) keep tree-sitter native bindings and onnxruntime off the bind mount — the actual Win/Mac perf win. - Credential mount paths are pre-created in the Dockerfile with `chown node:node` BEFORE `USER node`, so empty named volumes inherit correct ownership on first mount and first-run logins don't EACCES. - `CURSOR_API_KEY` is injected via `containerEnv: ${localEnv:CURSOR_API_KEY}` (Cursor's documented headless path); falls back to interactive `cursor-agent login` when the host env var is unset. Build-arg promotion - Build args (`CLAUDE_CODE_VERSION`, `CODEX_VERSION`, `CURSOR_VERSION`, `TZ`) are promoted to ENV in the Dockerfile so lifecycle commands and shells can resolve them. Without this promotion, Docker ARG values are build-only and silently no-op at lifecycle time. Workspace setup - `postCreateCommand` chowns the four workspace `node_modules` volumes (Docker creates them root-owned), then installs in dependency order: root → gitnexus-shared (install + build) → gitnexus → gitnexus-web. The shared package must build before its consumers (`file:../gitnexus-shared`). Ports - 5173 (Vite dev) and 4173 (Vite preview) auto-forwarded. - 4747 (`gitnexus serve`) marked `requireLocalPort: true` because `gitnexus-web/src/services/backend-client.ts` hardcodes `http://localhost:4747` as the default backend URL; a remapped port would silently break the web UI. VS Code integration - Recommended extensions: `anthropic.claude-code`, `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode`, `eamodio.gitlens`. - Settings: format-on-save with Prettier, ESLint auto-fix on save, zsh as default terminal profile, persistent zsh history via `HISTFILE` → `/commandhistory`. Documentation - `.devcontainer/README.md` covers WSL2 setup (clone inside WSL2 for IO and file-watcher reliability), first-time auth flows for each CLI, port- forwarding notes, LadybugDB container limitations, and the bumping procedure for each CLI version. - `CONTRIBUTING.md` gets a "Containerized development (optional)" subsection pointing at the devcontainer README. Deferred to a follow-up PR - Opt-in egress firewall (originally planned as a fourth implementation unit). The Dev Containers spec makes `runArgs` static — toggling `NET_ADMIN`/`NET_RAW` capabilities cleanly requires either a separate `devcontainer-firewall.json` profile or an `initializeCommand`-generated overlay. Keeping this PR focused on the working baseline. - Codespaces-specific tuning (works incidentally when the firewall is off, not actively tested). - Inside-container Playwright e2e (needs Chromium libs not in the base image). Verification deferred to user - This change introduces a new dev tooling artifact. Validate by running `docker build .devcontainer/`, opening the repo in VS Code via "Dev Containers: Reopen in Container", confirming `claude --version`, `codex --version`, `cursor-agent --version` resolve inside the container, and `cd gitnexus && npm run test:unit` runs clean against the named-volume `node_modules`. * fix(devcontainer): make interactive login the default auth path for all CLIs The previous `containerEnv` injected `CURSOR_API_KEY: "${localEnv:CURSOR_API_KEY}"`. When the host had no `CURSOR_API_KEY` set, this resolved to an empty string and Docker injected `CURSOR_API_KEY=""` into the container. Cursor CLI treats a set-but-empty `CURSOR_API_KEY` as "use this key" rather than "fall back to stored login", which silently broke `cursor-agent login` on the most common path — users who hadn't explicitly opted into API key auth. Drop `CURSOR_API_KEY` from `containerEnv`. Login is now the unconditional default for all three CLIs (Claude Code, Codex CLI, Cursor CLI); the named-volume + Dockerfile-chown pattern keeps credentials persistent across container rebuilds for every login path. Reorganize the README's auth section to put login first for all three CLIs uniformly (matching the new behavior) and move API key authentication into a separate "Alternative" section for CI/headless use. Document that API keys are intentionally not auto-propagated from the host and explain the export-in-shell or VS Code dotfiles-repo paths for users who want them. Update the troubleshooting row to reflect the new design. * fix(devcontainer): install gitnexus-web before gitnexus in postCreateCommand The previous order (root → gitnexus-shared → gitnexus → gitnexus-web) broke at the `gitnexus` install step because `gitnexus`'s `prepare` script runs `scripts/build.js`, which compiles `gitnexus-web` whenever its source tree exists. In the devcontainer the entire workspace is bind-mounted, so `gitnexus-web/` is present from the start — but its `node_modules/` wasn't yet, so `tsc -b` failed with: error TS2688: Cannot find type definition file for 'vite/client' error TS2688: Cannot find type definition file for 'node' Reorder so `gitnexus-web` installs before `gitnexus`. Verified end-to-end via `npx @devcontainers/cli up`: container builds clean, all three CLIs (Claude 2.1.153, Codex 0.134.0, Cursor) respond, and `npx tsc --noEmit` inside `/workspace/gitnexus` passes. Production Dockerfiles (`Dockerfile.cli` etc.) don't hit this because they only COPY `gitnexus/` + `gitnexus-shared/`, so `gitnexus-web/` doesn't exist at install time and `scripts/build.js` skips the web step. The devcontainer's full-tree bind mount changes that calculus. * fix(devcontainer): clear stale .husky/_ before npm install When `npm install` runs the root `prepare` script (husky), husky tries to copyfile `node_modules/husky/husky` → `.husky/_/h`. On Docker Desktop Windows bind mounts, if `.husky/_/` already exists from a prior container run, the new container's `node` user can't overwrite it via the bind mount's permission translation and the install fails with: Error: EPERM: operation not permitted, copyfile '/workspace/node_modules/husky/husky' -> '.husky/_/h' Drop `.husky/_` defensively in `postCreateCommand` before `npm install` so husky always starts from a clean slate. `.husky/_` is a husky runtime cache (gitignored), so removing it has no effect on the repo — husky regenerates it. No-op for WSL2-side checkouts (where this class of bind-mount permission collision doesn't occur). Add a troubleshooting row to `.devcontainer/README.md` covering the manual recovery (`rm -rf .husky/_` on the host) and the long-term fix (clone in WSL2 — Windows-side bind mounts will keep biting on this kind of issue across rebuilds with different UID alignment). * feat(devcontainer): bind-mount host CLI config dirs for plugin/skill/memory sync Switch the credential/config mounts from per-devcontainer named volumes to bind mounts of `${localEnv:HOME}/.claude`, `~/.codex`, and `~/.cursor`. Effect inside the container: - Authentication is shared with the host. If you've already run `claude login` / `codex login --device-auth` / `cursor-agent login` on the host, you're already authenticated in the container. - Plugins, skills, agents, memory, and settings sync both ways. Install a plugin in the container, it shows up on the host; add a custom agent on the host, the container sees it immediately. - All devcontainers on the host share the same CLI state, mirroring how host shells already share it. (Per-workspace isolation of plugins was never a stated requirement; the previous per-devcontainer named volumes leaked nothing useful.) Add `.devcontainer/ensure-host-config-dirs.cjs` and wire it as `initializeCommand`. It runs on the host before container create and guarantees `~/.claude`, `~/.codex`, `~/.cursor` exist, so Docker doesn't reject the bind mount when a CLI has never been used on this host. Cross-platform via Node `os.homedir()` + `fs.mkdirSync({recursive: true})`; idempotent; no third-party deps. Update `.devcontainer/README.md`: - New "How CLI state is shared with your host" section explaining the bind-mount model up front so users know their host plugins/skills/ memory carry into the container. - Mark first-time-login section as skippable when the user is already authenticated on the host. - Note the high-trust escape hatch: replace the three bind mounts with `type=volume` named volumes if the host/container trust boundary needs to be separated (Anthropic's reference pattern for enterprise). - Replace the obsolete "rm named volume" troubleshooting row with one that covers EACCES/EPERM on the host-bind-mount path. * refactor(devcontainer): address ce-code-review findings (P0 + 4 × P1 + 8 × P2 + 2 × P3) Walkthrough resolution of the 16-finding ce-code-review on PR #1875. 15 of 16 findings applied; one (F12, Anthropic Feature floating tag) was superseded by F6's Feature removal. P0 - F1: WSL2 is now REQUIRED for Windows hosts, not just recommended. ${localEnv:HOME} resolves to empty string on Windows-native (no HOME env var) — bind mounts then point at /.claude, /.codex etc. and silently break. ensure-host-config-dirs.cjs wrote to USERPROFILE-derived paths via os.homedir(), so the two surfaces disagreed about which env var was "home" on Windows. README header reframed; "Windows 11 — WSL2 is required" section explains the mismatch concretely. P1 - F2: Workspace `node_modules` volume names now include `-${devcontainerId}` so two GitNexus checkouts on the same host (~/work/GitNexus and ~/projects/GitNexus) don't share volumes and corrupt each other's installs. - F3 + F5: `postCreateCommand` extracted to `.devcontainer/post-create.sh` with `set -euo pipefail` and six labeled echo steps so failure logs name the step instead of an opaque &&-chain index. Chown step extended to cover /home/node/.npm, /commandhistory, and /home/node/.local — these named-volume mount points were owned by build-time UID 1000 but the container's `node` is re-IDed at runtime by updateRemoteUserUID on non-1000 Linux hosts, leaving them unwritable until now. - F4: Cursor installer downloaded to a temp file with curl --retry + --max-time; sha256 logged to build output before execution so drift across rebuilds is visible in CI logs. Full hard-pin (to a versioned downloads.cursor.com tarball with verified sha256) tracked as a follow-up in README "What's not included". P2 - F6: Anthropic Feature replaced with a direct `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}` so CLAUDE_CODE_VERSION actually pins the installed binary (the Feature ignored the ARG and pulled latest at install time). Honors the earlier "pin known-good versions" decision and resolves F12's floating-tag concern for this Feature. - F7: Dockerfile ARG defaults dropped for the three version vars; `devcontainer.json` `build.args` is now the single source of truth. Standalone `docker build .devcontainer/` must pass --build-arg. - F8: ensure-host-config-dirs.cjs deleted; `initializeCommand` now uses POSIX `mkdir -p` + `touch ~/.gitconfig` directly, dropping the host-Node-on-PATH prerequisite that broke on fresh Windows+Docker Desktop installs without Node. - F9: ~/.gitconfig bind-mounted read-only so `git commit` inside the container uses the host's user.name / user.email. Read-only so container-side `git config --global` doesn't leak to host. - F10: ~/.config/gh bind-mounted (read-write) so `gh pr create` / `gh pr checks` / `gh issue create` work inside the container without re-auth. AGENTS.md's commit + PR workflow now fully functional for agents inside the container. - F11: CLAUDE_CONFIG_DIR removed from Dockerfile ENV; canonical value lives only in devcontainer.json containerEnv. Eliminates the two-file edit risk. - F13: Mounts comment now documents per-instance vs per-workspace-name scoping rationale so future contributors don't guess. - F14: README "Trust boundary, concretely" paragraph names the exfil path explicitly (malicious npm postinstall → OAuth tokens → ~/.claude/projects/<workspace>/memory/MEMORY.md secrets) and lists vendor-side rotation runbook entries. P3 - F15: Dockerfile pre-create + chown of /home/node/.claude, .codex, .cursor dropped — those paths are bind-mounted, which fully shadows any image-side ownership. Only .npm, .local, /commandhistory still benefit from the pre-create. - F16: README "Bumping CLI versions" section rewritten against the post-F6 reality: CLAUDE_CODE_VERSION and CODEX_VERSION are real pins; CURSOR_VERSION is informational only. Verified locally: `docker build .devcontainer/ --build-arg ...` succeeds. Smoke-tested image: `claude --version` (2.1.153), `codex --version` (0.134.0), `cursor-agent --version` all resolve as the non-root `node` user; named-volume mount points (/home/node/.npm, /commandhistory) are node-owned at build time so non-1000 host UIDs get the post-create.sh chown fix instead of EACCES. * fix(devcontainer): cross-platform initializeCommand + soften Windows-native posture The previous commit's `initializeCommand` was POSIX-only (`mkdir -p $HOME/...`). VS Code on Windows runs the host shell as `cmd.exe /c ...`, which can't parse POSIX syntax — `$HOME` doesn't expand, `mkdir -p` errors, the init fails with `The syntax of the command is incorrect`, and container creation aborts before Docker is invoked. Switch `initializeCommand` to the spec's OS-keyed object form: - linux/darwin (covers WSL2 because VS Code runs initializeCommand in the WSL shell when attached via the WSL extension): POSIX mkdir+touch, as before - win32: PowerShell snippet that creates the same directories under $USERPROFILE and touches the gitconfig if missing Soften the README's hard "WSL2 required" framing from the previous commit. Reality per `@devcontainers/cli read-configuration` output: `${localEnv:HOME}` on Windows-native resolves to `C:\Users\<name>` (VS Code falls back to USERPROFILE), so the bind mount sources are valid Windows paths and Docker Desktop handles the translation. The earlier `accessing specified distro mount service` failure was a separate Docker Desktop WSL-integration issue, not a HOME-resolution issue. Windows-native works; it's just slower with more bind-mount permission edge cases (the husky/_/h EPERM class). The README now explains the tradeoff and steers toward WSL2 for performance + file watchers + permission reliability, rather than blocking Windows-native checkouts outright. Update the troubleshooting row to reflect the new posture. * fix(devcontainer): Node-based initializeCommand; bind-mount .ssh + .config/git Two fixes bundled: 1. The previous commit's OS-keyed `initializeCommand` object was based on a misread of the Dev Containers spec. The object form on command properties is **named parallel tasks**, not OS dispatch — VS Code ran all three keys in parallel via cmd.exe on Windows, the POSIX branches failed, and container creation aborted before Docker was invoked. Restore the single-string Node-based form: `node .devcontainer/ensure-host-config-dirs.cjs`. Node works identically in cmd.exe on Windows and bash/zsh on Linux/macOS/WSL, and `os.homedir()` respects $HOME on POSIX and %USERPROFILE% on Windows. The script is idempotent (mkdirSync recursive is a no-op for existing dirs; touch is gated on .gitconfig existence). Document Node ≥18 on the host as the only host-side prerequisite beyond Docker Desktop and the VS Code Dev Containers extension. Anyone running Claude Code on the host already has it. 2. Extend the host-bind mount surface with `~/.ssh` and `~/.config/git`, both read-only: - `~/.ssh` lets commit signing + push over SSH remotes work inside the container without copying private keys. Read-only mount means container code can read keys but can't modify or delete them. (Threat: a malicious dep can still read private keys from inside the container; the read-only mount narrows write-side blast radius, not read-side. Documented in the trust-boundary section.) - `~/.config/git` covers XDG-style git config (`~/.config/git/config`, `~/.config/git/ignore`, `~/.config/git/attributes`) for users who keep settings there instead of `~/.gitconfig`. Read-only, same as `~/.gitconfig`. Update the CLI-state-sharing table and trust-boundary paragraph to reflect the expanded surface. Re-adds .devcontainer/ensure-host-config-dirs.cjs (deleted before the OS-keyed attempt). * fix(devcontainer): fail-fast on Windows-native with HOME-not-set diagnostic The previous commit's "Windows-native works" softening was wrong. VS Code on Windows-native resolves `${localEnv:HOME}` by reading the host shell's HOME env var, and cmd.exe has no HOME set — the bind sources collapse to `/.claude`, `/.codex`, etc., and Docker errors: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /.claude The @devcontainers/cli output that prompted the softening was misleading because I ran it from a Bash session with HOME already set, not from VS Code's cmd.exe call context. The original Finding-1 P0 — that Windows- native silently breaks the bind-mount feature — was correct. Three changes: 1. `ensure-host-config-dirs.cjs` detects the failure mode early: `if (process.platform === 'win32' && !process.env.HOME)` prints a targeted error message naming the root cause (cmd.exe has no HOME → ${localEnv:HOME} resolves empty → bind sources fail) and a step-by-step pointer to set up WSL2. Exits 1 so VS Code surfaces it as a clean container-creation failure, not the cryptic Docker bind-mount error. 2. README header reverted to "Windows 11 via WSL2" only (not "and Windows-native"). The "Windows 11 — WSL2 is required" section names the specific HOME-resolution mismatch concretely so future readers understand why the constraint exists. 3. Troubleshooting table gets a new row for the `ERROR: GitNexus devcontainer requires WSL2` message pointing at the setup section. * feat(devcontainer): support Windows-native via auto setx HOME on first run Reverses the "WSL2 required on Windows" posture. Windows-native now works after a one-time auto-handled setup. The root cause of the bind-mount failure: VS Code resolves `${localEnv:HOME}` by reading its own process env, and Windows doesn't set `HOME` by default — Windows uses `USERPROFILE`. So the bind sources were collapsing to `/.claude`, `/.codex`, etc., and Docker rejected them. `ensure-host-config-dirs.cjs` now handles this automatically on Windows hosts where `HOME` is unset: 1. Runs `setx HOME "%USERPROFILE%"`, which writes to the user-level Windows environment (HKCU\Environment) — no admin required. Every future user process inherits HOME from there. 2. Prints a clear one-time setup banner explaining the user needs to fully restart VS Code (File > Exit, not just close the window) for VS Code to pick up the new env at its next startup. 3. Exits 1 so VS Code surfaces this as a clean container-create failure instead of letting Docker error opaquely later. On the second Reopen-in-Container attempt, `HOME` is now set in VS Code's env, the script skips the setup block, creates the bind-mount source dirs, and the container builds normally. Subsequent rebuilds have no extra steps. Mac, Linux, and WSL2 hosts have `HOME` set by the shell, so the new block is a no-op there. Same `devcontainer.json` works across all supported hosts. README rewritten to reflect the new posture: - Header lists Windows 11 (native) as a supported host alongside macOS, Linux, and WSL2, with a note that Windows-native gets a one-time HOME setup handled by the initializeCommand. - New "Windows 11 setup" section walks through the auto-handled setup flow + a manual `setx HOME "%USERPROFILE%"` fallback for users who want to do it themselves. - "Known trade-offs of Windows-native vs WSL2" subsection lays out the Docker Desktop Windows bind-mount edge cases (file watchers, npm install perf, husky/_ EPERM) so users opting into Windows-native do so eyes-open. WSL2 remains documented as the faster path for users who want it, but it's no longer the only supported one. - Troubleshooting table gets two new rows: the one-time setup banner (with "what to do" instructions) and the residual `bind source path does not exist` case (run setx manually + fully exit VS Code). * fix(devcontainer): drop ~/.gitconfig bind mount; defer to VS Code auto-copy VS Code's Dev Containers extension auto-copies the host's gitconfig into the container at attach time using `(dd ...) >> /home/node/.gitconfig`. A read-only bind mount of ~/.gitconfig blocks that write, so attach failed with `cannot create /home/node/.gitconfig: Read-only file system`. Making it read-write would let the append succeed, but the bind mount means the host file and the container file are the same file — VS Code's append would double the host gitconfig contents on every container start. Drop the ~/.gitconfig bind mount entirely. VS Code's auto-copy is the purpose-built mechanism for this, gives the container the host's user.name / user.email transparently, and avoids both the read-only write failure and the append-duplication trap. The container ends up with a writable /home/node/.gitconfig that's a copy of the host's, not a mount. The remaining six bind mounts (.claude, .codex, .cursor, .ssh, .config/git, .config/gh) keep their existing modes — XDG-style git config under ~/.config/git is unaffected by VS Code's auto-copy (which only targets ~/.gitconfig), so its read-only bind mount stays. Also remove the `.gitconfig` touch from ensure-host-config-dirs.cjs (now unnecessary) and update the README CLI-state table, sharing explanation, and troubleshooting row to reflect that gitconfig flows in via VS Code auto-copy rather than the bind mount. * feat(devcontainer): bind-mount ~/.docker, ~/.aws, ~/.azure for agent workflows Extend the host bind-mount surface so coding agents inside the container inherit cloud + container-registry auth from the host without any per-container setup: - ~/.docker (read-write) — Docker registry auth (config.json) + buildx config. Container-registry pushes (ghcr.io, docker.io) from inside the container pick up host `docker login` state. Read-write because the Docker CLI refreshes credential-helper tokens. - ~/.aws (read-only) — AWS CLI / SDK credentials. Read-only because rotating creds typically happens via the host. Empty on this dev box, so forward-compatible: the moment you `aws configure` on the host the container picks it up on the next rebuild. - ~/.azure (read-only) — Azure CLI credentials. Same pattern as ~/.aws. `ensure-host-config-dirs.cjs` extends to mkdir these three on init so the bind mounts always have a valid source even if a CLI has never been used on this host. The Docker CLI itself isn't installed in the container by default — the ~/.docker/ mount is inert until you add `docker-outside-of-docker:1` or similar Feature. README now calls this out under "What you still don't have inside the container" so it's obvious which CLIs are agent-ready and which need a feature add to become useful. README updates: - Bind-mount table gains a "Why" column and rows for the three new mounts, making it clear at a glance what each one enables. - Trust-boundary section lists Docker registry tokens, AWS, and Azure creds in the read-side exfil path so the threat model stays honest as the credential surface grows. - New subsection lists not-included CLIs (Docker, AWS, Azure, gcloud, kubectl, private-npm) with the exact Feature ID or mount snippet needed to enable each — turns "I want my agent to do X" into a one-line config change. Verified locally: `npx @devcontainers/cli read-configuration` resolves all 9 host bind mounts to valid C:\Users\<name>/* paths on Windows. * refactor(devcontainer): hybrid AI CLI config — read-only host share + per-container credentials Restructure the Claude Code / Codex / Cursor mount topology to fix the silent first-run-UI bug surfaced in PR testing, and to harden against the host-write-through escape class the previous bind-mount design exposed. The actual root cause of the first-run wizard firing on the user's screenshot — confirmed via three parallel research agents (best practices, framework docs deep dive of the OpenAI Codex Rust source, adversarial design review) — was NOT a credential permission check. Claude Code splits state across `~/.claude/.credentials.json` AND `~/.claude.json` (a FILE at $HOME, sibling of the `.claude/` dir). The latter holds `hasCompletedOnboarding`, `userID`, `oauthAccount` metadata, MCP user-scope config, and per-project trust state — and Claude Code reads it at literal `$HOME/.claude.json`, not via `CLAUDE_CONFIG_DIR`. The previous design mounted `~/.claude/` but left `~/.claude.json` outside the topology entirely, so every container started with a missing onboarding-state file and re-ran the wizard. Confirmed by tfvchow/field-notes-public#10: "Persisting .credentials.json alone is NOT sufficient. Without .claude.json, Claude Code treats the session as a fresh install and prompts for login regardless of valid credentials being present." The new topology: **Mounts** - `${localEnv:HOME}/.claude` → `/host/.claude` (read-only bind) - `${localEnv:HOME}/.codex` → `/host/.codex` (read-only bind) - `${localEnv:HOME}/.cursor` → `/host/.cursor` (read-only bind) - `${localEnv:HOME}/.claude.json` → `/host/.claude.json` (read-only bind) - `claude-config-${devcontainerId}` → `/home/node/.claude` (named volume) - `codex-config-${devcontainerId}` → `/home/node/.codex` (named volume) - `cursor-config-${devcontainerId}` → `/home/node/.cursor` (named volume) **containerEnv** gains `CODEX_HOME=/home/node/.codex` (Codex's own env override, per its public Rust source). `CLAUDE_CONFIG_DIR=/home/node/ .claude` was already set. **`post-create.sh`** stages the named volumes on first run: - Symlinks shareable subdirs from `/host/.claude` into the named volume: `plugins/`, `skills/`, `agents/`, `memory/`, `commands/`. Codex gets `config.toml` symlinked. Cursor has no shareable subdirs (cli-config .json conflates auth and settings). - Copies `.credentials.json`, `auth.json`, `cli-config.json` on first run with `chmod 600`. After first run, container manages its own refresh; host's credentials untouched. - Copies `~/.claude.json` on first run (with stub `{"hasCompletedOnboarding":true,"installMethod":"global"}` fallback for hosts that haven't run Claude Code). This is the fix for the observed onboarding-wizard loop. `ensure-host-config-dirs.cjs` now also touches `~/.claude.json` on the host if missing, so the bind mount has a valid source on hosts that have never run Claude Code. **Why read-only + named volume vs. the previous full bidirectional bind mount:** 1. **Host filesystem write-through escape, eliminated.** Previous design symlinked `plugins/`, `agents/`, `skills/` write-through into the host's `~/.claude/` — a malicious npm package in the workspace dep tree could drop `agents/evil.md` into the host's config, which the next host Claude session would auto-load. The read-only `/host` mount blocks this; container compromise no longer persists across teardown via host-side autoload. 2. **Windows bind-mount perm-flattening, sidestepped.** Files surfaced through a Docker Desktop Windows bind mount appear as `root:root` mode `777`. Credentials in the named volume come with proper Linux ownership and `chmod 600` — what each CLI expects on write (none enforces on read, but write-side hygiene matters for the host's understanding of "where credentials live"). 3. **No `ide/` lock-file collisions.** Previous design symlinked `~/.claude/ide/` write-through, including per-PID lock files. Host PID and container PID namespaces are unrelated → lock-file PIDs misclassify dead processes as alive. Skipping `ide/` keeps lock files container-local. 4. **No `projects/` ghost dirs.** Host encodes the workspace path as `D--development-coding-GitNexus`, container as `-workspace`. Bidirectional `projects/` symlinks would split memory and session state across two ghost project dirs for what is conceptually the same project. Skipping `projects/` keeps per-project state container-local; host's projects/ stays untouched. 5. **No `settings.json` version drift.** Container is pinned to a specific Claude Code version (`CLAUDE_CODE_VERSION` build arg); host floats with auto-update. Bidirectional `settings.json` writes produced silent schema rollback. Skipping settings.json keeps each side authoritative for its own version. **README** rewritten in the same section to describe the new topology honestly: what's shared, what isn't, the OAuth refresh-token divergence between host and container, per-CLI quirks (macOS Keychain storage, Cursor's known upstream in-container auth bug, Codex keyring storage). Trust-boundary section updated to name the threat model accurately — same read surface as before (malicious dep can still READ all credentials), but write-through into host plugin/agent dirs is now blocked. Verified locally: `@devcontainers/cli read-configuration` resolves all 19 mounts correctly on Windows, `post-create.sh` parses, and `ensure-host-config-dirs.cjs` idempotently touches `~/.claude.json`. Research backing this design: - Anthropic Claude Code devcontainer docs (named-volume pattern): https://code.claude.com/docs/en/devcontainer - tfvchow/field-notes-public#10 (both files required): https://github.com/tfvchow/field-notes-public/issues/10 - anthropics/claude-code#29029 (VS Code extension strips hasCompletedOnboarding): https://github.com/anthropics/claude-code/issues/29029 - OpenAI Codex Rust source (no read-side perm check): https://github.com/openai/codex/blob/main/codex-rs/login/src/auth/storage.rs - Cursor CLI in-Docker auth issue: https://forum.cursor.com/t/cursor-agent-authentication-issue-inside-docker/143995 * fix(devcontainer): resync AI CLI state from host on every container-create Two bugs were causing Claude Code to fire the onboarding wizard inside the container even with valid host credentials: 1. Missing the second state file. Claude Code 2.1.x writes a small `.claude.json` INSIDE `CLAUDE_CONFIG_DIR` (carrying migration tracking + userID), not just the one at `$HOME/.claude.json`. If the userIDs in the two files disagree, Claude treats the session as inconsistent and re-onboards. The previous post-create.sh only copied the `$HOME` one. 2. First-run guards (`[ ! -e $dst ]`) skipped the copy when stale named volumes from earlier rebuilds still had the prior session's state in them, leaving the container desynced from the host. Replace `copy_on_first_run` with `sync_from_host` that always overwrites from host on container-create. `link_readonly_share` now clears stale non-symlink dst entries before linking. Copies both `$HOME/.claude.json` and `$CLAUDE_CONFIG_DIR/.claude.json` so userIDs stay aligned. Container can still mutate its own state between rebuilds; resync only happens on rebuild (postCreate boundary). * docs(devcontainer): document sync-from-host design + dual-source auth flow README still described the old "first-run copy" behavior. After the post-create.sh change to always-sync-from-host, the design works either direction: - Log in on host → next container-create syncs the credentials into the named volume. - Log in inside the container → the named volume persists the login across rebuilds; the host has no source to overwrite from, so it stays alone. Also documents the two-Claude-state-files trap (`$HOME/.claude.json` AND `$CLAUDE_CONFIG_DIR/.claude.json`, both with the same userID required), and the volume-deletion recovery path for stale named volumes carried over from earlier rebuilds. * fix(devcontainer): full plugin/config parity by dropping CLAUDE_CONFIG_DIR + syncing settings.json Two changes that together give the container the same plugins and configs as the host for all three AI CLIs (login stays per-container): 1. Drop CLAUDE_CONFIG_DIR from containerEnv. The named-volume mount target `/home/node/.claude` already matches Claude's default `~/.claude`, so the env var added no behavior — but setting it changed which file Claude reads `hasCompletedOnboarding` from. With it set, Claude reads `$CLAUDE_CONFIG_DIR/.claude.json` (the small identity-only file that does NOT carry `hasCompletedOnboarding`); without it, Claude reads `$HOME/.claude.json` (the big onboarding-state file that does). The wizard fires every container-create when set, skips when unset. 2. Sync `settings.json` from host (Claude) + symlink `memories/` and `skills/` from host (Codex). Theme + `enabledPlugins` + `extraKnownMarketplaces` live in `settings.json` — without syncing it, the theme picker fires and host-installed plugins stay disabled even though their files are symlinked in. Codex's `memories/` and `skills/` are the symmetric Codex user-installed surface, now shared the same way Claude's plugins/skills/agents/memory/commands are. Cursor stays as-is — `cli-config.json` conflates auth+settings (already synced), and there's no separate plugin surface to mirror. Login details remain per-container by design (acceptable to re-login on rebuild). Everything else — plugins, skills, agents, memory, MCP user- scope config, project trust, theme, plugin enablement — now matches host on every container-create. * refactor(devcontainer): hybrid RW bind + per-container creds — fixes EROFS on in-container plugin install The previous Option B topology (RO host stage + named volume + symlinks into the volume) made `/plugin marketplace add` inside the container fail with EROFS — the symlinks pointed at a read-only mount, so Claude couldn't create new marketplace dirs. Switch to a hybrid: shareable content (plugins/skills/agents/memory/commands/settings.json/$HOME/.claude.json for Claude; config.toml/memories/skills for Codex) gets a direct RW bind from host so reads and writes go bidirectionally; credentials + the small identity file stay in per-container named volumes so logout in container doesn't log out host. Mount precedence does the heavy lifting: the named volume mounts at /home/node/.<cli> first, then sub-path bind mounts overlay specific sub-paths. Container's view at /home/node/.claude/plugins/ is the host dir; container's view at /home/node/.claude/.credentials.json is the named volume's file. What this gives you: - /plugin marketplace add in container = installed on host - New skill on host = visible in container immediately (no rebuild) - claude logout in container = host stays logged in - compound-engineering plugin enabled on host = enabled in container - Theme picker fires once (or never if host has theme set) What it costs: - Write-through: a compromised npm dep in workspace deps can write to host ~/.claude/{plugins,skills,agents,memory,commands}/. Documented trade-off; for personal dev, accepted. Credentials still per-container. post-create.sh becomes much simpler — only syncs the four credential files from host into the named volumes. No more symlink dance, no more state-file merging. ensure-host-config-dirs.cjs gains the new bind sources: the shareable subdirs and settings.json/config.toml files get mkdir/touched on host so Docker doesn't reject the mount when a CLI has never been used. * fix(devcontainer): translate host plugin registry paths to Linux on rebuild The previous topology bind-mounted the entire `~/.claude/plugins/` directory from host. That brought through plugins, marketplaces, and extracted cache content correctly — but ALSO brought through the registry JSONs (`known_marketplaces.json`, `installed_plugins.json`, `plugin-catalog-cache.json`) which carry absolute OS-native paths: "installLocation": "C:\Users\gergo\.claude\plugins\marketplaces\X" "installPath": "C:\Users\gergo\.claude\plugins\cache\Y\Z" Claude in the Linux container fails to resolve these Windows paths and reports `Marketplace X failed to load: cache-miss`. Split the topology: - `plugins/marketplaces/` (git clones) and `plugins/cache/` (extracted plugin files) stay bidirectional RW binds — content is path-independent. - Registry JSONs move into the per-container named volume. post-create.sh reads host's versions, rewrites any absolute path ending in `/.claude/plugins/<rest>` (Windows `C:\Users\...` and POSIX `/Users/...` / `/home/...` patterns) to `/home/node/.claude/plugins/<rest>`, and writes the translated result to the volume. What this gets you: - Plugin installed on host → next container rebuild has it (translated). - Plugin installed inside container → lives in volume registry; lost on rebuild (consistent with credentials model). Re-install on host for persistence. ensure-host-config-dirs.cjs now also creates `plugins/marketplaces/` and `plugins/cache/` on host if absent (Docker rejects bind mounts whose source doesn't exist). * fix(devcontainer): clean stale plugin/skill symlinks from prior design before writes A user upgrading from Option B (read-only host stage + symlinks) to the current hybrid RW-bind topology hit EROFS in post-create.sh when the plugin registry path-translator tried to write `/home/node/.claude/plugins/known_marketplaces.json`. The named volume still carried `/home/node/.claude/plugins -> /host/.claude/plugins` (Option B's symlink). The new design's sub-path bind mounts at `plugins/marketplaces` and `plugins/cache` overlay through the symlink, but writes to the parent dir itself resolve via the symlink to the RO host stage and fail. Drop any leftover symlinks at known target paths early in step 2 so the mkdir/writes that follow land in the volume. * refactor(devcontainer): split workspace-deps to updateContentCommand post-create.sh was doing two unrelated jobs: workspace dependency install (four `npm install` runs in topological order) and AI CLI credential sync. They have different lifecycle needs — deps should re-run when lockfiles change, AI sync should run once per container — but both were gated on container-create. Per Dev Container spec lifecycle, `updateContentCommand` is the right hook for workspace deps: runs at container-create AND on content changes (lockfile updates). `postCreateCommand` is right for AI CLI sync: container-create only. Move steps 3-7 (husky cleanup + four `npm install` runs) into install-deps.sh wired as `updateContentCommand`. Split the chown step too — install-deps owns workspace-side dirs (node_modules volumes, ~/.npm), post-create owns AI-side dirs (~/.claude, ~/.codex, ~/.cursor, /commandhistory, ~/.local). Each script now has one concern. post-create.sh drops from ~187 lines to 148; install-deps.sh is 56 lines new. Faster rebuilds when nothing about deps changed (the credential sync + path translation work still runs every container-create, but the npm install dance no longer does). Research backing (no other simplification applies): - Anthropic's reference devcontainer uses pure named volumes; no host-state inheritance pattern is published. - Path translation has no upstream fix (issues #21916, #10379 closed without resolution). Our Node rewrite is the workaround. - pnpm workspaces (`pnpm -r install`) would replace the four installs with one command, but that's a real refactor (touches gitnexus/scripts/build.js + 4 package.json files); deferred. - `HUSKY=0` in containerEnv would drop the `rm -rf .husky/_` hack, but would also stop pre-commit hooks from firing inside the container; deferred. * fix(devcontainer): drop single-file binds — fixes Codex `batchWrite failed in TUI` On Docker Desktop Windows the named volumes are ext4 (`/dev/sdd`) while single-file bind mounts from the Windows host land as 9p (drvfs). Different filesystems → atomic config writes (write `foo.tmp`, then rename onto `foo`) trip EXDEV `inter-device move failed` / `Device or resource busy`. Codex's TUI surfaces this as `config/batchWrite failed in TUI` when saving model preference. Claude's writes to settings.json / .claude.json fail the same way, silently. Reproduction in container: $ echo x > /tmp/foo.toml; mv /tmp/foo.toml /home/node/.codex/config.toml mv: inter-device move failed: ... Device or resource busy Fix: drop the three single-file bind mounts. Sync host's versions into the named volume on container-create via `sync_from_host` (same pattern already used for credentials). Atomic rename within the volume works because everything is ext4. Trade-off: container writes to these files no longer propagate to host; they stay in the volume until next rebuild, which re-syncs from host. Host is source of truth on rebuild — same model as credentials. Plugin/ skill/agent/memory/command DIRS still bind-mount bidirectionally (atomic writes within a dir bind stay on one filesystem, no EXDEV). Files affected: - ~/.codex/config.toml - ~/.claude/settings.json - ~/.claude.json (HOME-level — added `/host/.claude.json` RO mount back for sync_from_host to read) * chore(autofix): apply prettier + eslint fixes via /autofix command * feat(devcontainer): Codex + Cursor plugin/config host parity with Claude Codex plugins installed in the container never reached the Windows host because, unlike Claude, the Codex plugin tree wasn't bind-mounted — only memories/ and skills/ were. Verified via live /proc/mounts: Claude binds 6 shareable dirs (incl. plugins/marketplaces + plugins/cache), Codex bound 2. So `codex plugin add` wrote into the ext4 named volume and stayed there. Codex changes: - Bind the WHOLE ~/.codex/plugins dir + ~/.codex/prompts (plus existing memories/skills). Strace of two real `codex plugin add` runs proved the installer stages INSIDE plugins/cache/<marketplace>/ and renames intra-dir, so a single 9p bind of plugins/ keeps the rename intra-fs — no EXDEV (the bug that broke single-file binds). .tmp/ stays on the volume (it's the cross-fs staging source). No path translation needed: Codex enablement lives in config.toml as git URLs, not FS paths. - Verified live: `codex plugin add compound-engineering@...` now writes through to C:\Users\...\.codex\plugins\cache\ on the Windows host, and host-created files appear in the container (bidirectional). Cursor changes (review found cursor-agent has a real plugin surface, not editor-only — Cursor 2.5 Marketplace shared by IDE + CLI): - Bind plugins/marketplaces, plugins/local, rules, commands, agents, skills (dir binds, EXDEV-safe). - Copy-on-create mcp.json (single file → EXDEV-unsafe as bind), alongside the existing cli-config.json. - Translate plugins/installed_plugins.json (carries absolute Windows paths like Claude's) — generalized the existing path-rewrite to run for both Claude and Cursor. - hooks.json deliberately NOT shared (runs shell commands → supply-chain surface); documented as opt-in. ensure-host-config-dirs.cjs pre-creates all new host bind sources. post-create.sh defensive symlink cleanup extended to the new Codex/Cursor paths. README updated with the accurate per-CLI share/sync/translate matrix. Design adversarially verified (straced installs, EXDEV primitive tests, sqlite-under-bind check, path-encoding check) before implementing. * fix(devcontainer): resolve ce-code-review findings (doc drift, chown scope, .cjs extraction, CI smoke) Multi-agent review (9 reviewers) found the devcontainer files carried comments + README from the abandoned read-only-symlink design, plus real behavioral gaps. Resolved all actionable findings (no deferrals). Documentation drift (the headline — stale comments described a security model opposite to what shipped): - README "Trust boundary" claimed a malicious dep "cannot write back … the read-only /host mount blocks the write." FALSE — the shareable dirs are RW-bound. Rewrote to document the bidirectional write-through, what stays one-way (credentials never flow back), and how to close it. - devcontainer.json mount group-1 comment described "selectively symlinks … read-only eliminates write-through" — replaced with the RW-bind reality. - Header "Windows-native is unsupported" -> supported (auto HOME setup). - containerEnv comment "credentials persist in host-bind-mounted dirs" -> they live in the named volumes. - hooks.json exclusion documented honestly as a partial mitigation, not a clean boundary (commands/agents/skills/rules are equally executing). - ~/.local "named volume" -> image directory. Behavioral fixes: - chown -R recursed into the RW host binds (could rewrite host ownership / EPERM-abort provisioning on non-UID-aligned Linux). Switched to `find -xdev` per dir so chown stays on the volume filesystem. - Cursor installer wrapped in `timeout 300` — its inner binary download isn't covered by curl --max-time and could hang docker build forever. - Removed dead CURSOR_VERSION ARG/ENV/build-arg (never consumed; "latest" implied a pin the installer can't honor). Documented why Cursor is unpinned. Extraction + tests (the two inline post-create.sh node heredocs were unlintable and untestable; the path regex had had bugs): - seed-claude-config.cjs — installMethod-strip seed, now with a non-object guard (a bare-value/array host .claude.json could otherwise slip the try/catch and silently re-trigger onboarding) and labeled write errors. - translate-plugin-registries.cjs — plugin-registry path translation with labeled errors. - translate-plugin-registries.test.cjs — 12 tests (Windows/POSIX paths, cross-CLI isolation, nested objects, non-object/empty-config guard). - post-create.sh calls the modules via $SCRIPT_DIR. CI: - .github/workflows/ci-devcontainer.yml — runs the unit tests + shell syntax checks + a `@devcontainers/cli build` smoke on .devcontainer/** changes. Conforms to the repo concurrency convention (validator passes). Documented (real gaps, fixes are honest docs since no correct auto-fix exists): user-scope MCP servers with absolute host command paths don't resolve in-container; user-scope config is copy-on-create so host edits need a rebuild; in-container plugin installs get shadowed by an empty host bind on rebuild (recovery noted); plugin installs are single-writer across checkouts; gh/docker RW-vs-ssh/aws/azure-RO rationale. Verified: fresh `@devcontainers/cli up` succeeds; installMethod stripped, registry translated to Linux paths, credentials node:node, 12/12 tests pass. * fix(devcontainer): set persist-credentials:false on CI checkouts + prettier - zizmor `artipacked` (CodeQL/GitHub Advanced Security) flagged both actions/checkout steps in ci-devcontainer.yml: checkout defaults to persist-credentials:true, leaving GITHUB_TOKEN in .git/config where it can leak into uploaded artifacts. Both jobs are read-only (run tests / build smoke, never push), so persist-credentials:false is correct — matches the repo convention in codeql.yml / ci-tests.yml. - Ran prettier 3.8.0 over the new .cjs modules + test (single-quote/style normalization to match the repo). JSON/YAML were already compliant; README is in .prettierignore; .sh has no prettier parser. Behavior unchanged — 12/12 transform unit tests still pass. * fix(devcontainer): resolve adversarial review findings (pins, RO mounts, tests) Resolves the blocking + actionable findings from the PR #1875 review: - Pin base image by digest as bare name@digest [#1]. The :tag@digest form trips the @devcontainers/cli image-name parser (which builds this image in CI and in VS Code "Reopen in Container"); bare name@digest is the parser-compatible form. Verified by a full local build. - Pin Cursor by version + per-arch sha256 and fetch the artifact directly instead of executing cursor.com/install; fail-closed on mismatch [#2]. - Mount ~/.config/gh and ~/.docker read-only so a compromised dep can't rewrite the host GitHub token / Docker credHelper [#4]. - Pin @devcontainers/cli@0.87.0 in the CI smoke [#5]. - chown via find -xdev in install-deps.sh (symlink-safe; matches post-create.sh) [#6]. - Add filesystem-I/O tests (translate/readHostConfig/seed main/ensurePaths) and refactor ensure-host-config-dirs to be unit-testable [#7]. - Stop pre-creating settings.json/config.toml on the host; only the real single-file bind source (.claude.json) is touched [#10]. - Add a prominent top-of-README security callout for the RW write-through trade-off and reframe the deferred egress firewall as the key missing compensating control [#3, #9]. Full devcontainer build verified locally (digest pull + pinned Cursor download/extract/symlink). 24/24 config-transform tests pass. * fix(devcontainer): resolve local adversarial-review findings (low/nit) Follow-up to a local branch review (run after the cloud review crashed before producing findings); all 5 confirmed findings were low/nit: - chown via `find -xdev -exec chown -h`: add -h so chown acts on a symlink ITSELF, not its target. Without it a dangling node_modules/.bin link aborted provisioning under `set -e`, and a cross-fs symlink target could be dereferenced/rewritten. Verified in a clean container (regular files still chowned; dangling link no longer aborts; cross-fs target untouched). Applied to install-deps.sh and post-create.sh; the inline comments are corrected to describe -xdev (descent bound) and -h (no deref) as the two distinct guards. - Reword the .cjs header claims from "lintable" to "unit-tested and prettier-checked": ESLint applies no rules to .cjs in this repo; CI only prettier-checks them. - README: the initializeCommand is `node ensure-host-config-dirs.cjs`, which creates the full bind-source set, not a bash `mkdir -p` of four dirs. - ci-devcontainer.yml: document that the x64 runner exercises only the amd64 Cursor branch; the arm64 sha/URL is hash-pinned (verified against the published artifact) but not built in CI. - Make the seed chmod-644 test meaningful: pre-create dst at 0o600 so only the explicit chmodSync can widen it (the prior assertion passed under the default umask regardless of whether the chmod ran). 25/25 config-transform tests pass; arm64 + x64 Cursor artifacts verified. * docs(devcontainer): rewrite code comments in plain English The devcontainer comments had grown dense and jargon-heavy. Rewrite them across all 9 files into short, plain-English sentences — same facts and reasoning, just clearer wording. Comments only; no code changed. Verified: the diff touches comment lines only, 25/25 config-transform tests pass, devcontainer.json is still valid JSONC with build.args + readonly mounts unchanged, shell scripts pass `bash -n`, and prettier is clean. * feat(devcontainer): persist AI CLI session state across container recreation Add dedicated per-workspace named volumes (mount group 6) for the three AI CLIs' session/resume state so `claude --resume`, `codex resume`, and `cursor-agent resume` survive a rebuild, a full delete-and-recreate, and the `docker volume rm <cli>-config-*` re-login fix: - Claude -> ~/.claude/projects - Codex -> ~/.codex/sessions - Cursor -> ~/.cursor/chats + ~/.cursor/projects The volumes are SEPARATE from the credential/config volumes and keyed like the node_modules volumes (${localWorkspaceFolderBasename}-...- ${devcontainerId}), so wiping a config volume to force a re-login no longer destroys session history. Session state already survived a plain rebuild (it lived in the config volume); this closes the recreation, volume-rm, and devcontainerId-change gaps. Kept container-private (not host bind mounts) deliberately: transcripts can contain pasted secrets, so a host bind would spill them to host disk, widen the supply-chain write-through surface, and leak cross-project transcripts. A commented-out opt-in host-bind block is included for users who accept that trade-off. post-create.sh: chown each new volume root explicitly (find -xdev stops at the config-volume filesystem boundary and won't descend into them), guarded with `[ -d ] || continue` so a missing root can't abort provisioning under set -e. README: document the topology, what survives vs not, the one-time first-rebuild masking of pre-existing config-volume sessions, updated rebuild/reset commands, and the trust-boundary impact. * feat(devcontainer): isolate host AI-CLI config via seed-once copies + persist claude-mem Replace the read-write host bind mounts for the AI-CLI shareable dirs (Claude skills/agents/memory/commands/plugins; Codex plugins/prompts/ memories/skills; Cursor rules/commands/agents/skills/plugins) with a seed-once copy from a read-only /host/.<cli> stage into the per-container config volume. The container gets its own writable copy and can never write back to the host, closing the write-through vector where a compromised in-container dependency could drop a malicious agent, command, skill, or plugin onto the host for the next host session to auto-load. Add a per-container claude-mem named volume (claude-mem-${devcontainerId}) at /home/node/.claude-mem, seeded once from a read-only /host/.claude-mem stage. claude-mem's multi-GB SQLite + Chroma store is kept off a host bind (unreliable fcntl locking / corruption risk over 9p on Docker Desktop Windows) while still surviving rebuilds. - post-create.sh: seed shareable dirs (marker-gated, seed-once) and run plugin-registry translation per seeded CLI; seed claude-mem behind a completion-sentinel guard that self-heals an interrupted multi-GB copy; chown the claude-mem volume only on first create. - translate-plugin-registries.cjs: add selectRegistries() so translation runs per-CLI seed-once instead of clobbering container-installed plugins. - ensure-host-config-dirs.cjs: add ~/.claude-mem; drop the shareable subdirs (no longer bind sources). - devcontainer.json: drop the RW shareable binds; add the claude-mem volume + read-only stage. - README: rewrite trust-boundary, mount table, and rebuild/reset docs for the copy model. - tests: cover selectRegistries and the trimmed DIRS (30 pass). * feat(devcontainer): add Bun 1.3.14, pinned via build arg Installed by the official bun.sh/install script with the release tag passed as the first positional arg, so the version is pinned even though the install path itself is an unverified remote script (the one such exception in the image — Cursor and the base image stay sha256/digest- pinned). BUN_INSTALL is set in ENV so the binary lands at a known path and the installer's rc-file edits don't matter. unzip is added to apt since the Bun installer extracts a .zip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(devcontainer): persist gh auth via copy-into-volume model Move ~/.config/gh from a read-only bind to the same read-only host stage + per-container named volume pattern used for the AI CLI credentials. post-create.sh seeds hosts.yml/config.yml from the /host/.config/gh stage into the gh-config volume on create, so an in-container `gh auth login` now persists across rebuilds while the read-only stage still prevents any write-back to the host token. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(devcontainer): bump Claude Code to 2.1.156 for Opus 4.8 The pin was 2.1.153, which predates Opus 4.8 support (added in 2.1.154). With DISABLE_AUTOUPDATER=1 the container never updated past the pin, so Claude Code only offered models up to 4.7. Bump to the latest 2.1.156 so Opus 4.8 is available. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
henry201605
pushed a commit
that referenced
this pull request
Jun 4, 2026
…wari#909 Ring 3, closes abhigyanpatwari#940) (abhigyanpatwari#1950) * feat(vue): migrate Vue SFC to scope-based resolution (RFC abhigyanpatwari#909 Ring 3, closes abhigyanpatwari#940) Adds `vueScopeResolver` and wires Vue into the scope-resolution pipeline (`SCOPE_RESOLVERS`, `MIGRATED_LANGUAGES`). Vue's `<script>` / `<script setup>` blocks are TypeScript — `emitVueScopeCaptures` extracts the script block via the existing `extractVueScript` utility and delegates to `emitTsScopeCaptures`, keeping grammar identity consistent with the cached tree the parse-worker already builds. - `languages/vue/captures.ts` — `emitVueScopeCaptures` - `languages/vue/import-target.ts` — `makeVueResolveImportTarget` (TS resolver + tsconfig path-alias support; explicit `.vue` imports resolve via the exact-path branch) - `languages/vue/scope-resolver.ts` — `vueScopeResolver` - `languages/vue/index.ts` — barrel + known-limitations doc - `languages/vue.ts` — `emitScopeCaptures` hooked up - `scope-resolution/pipeline/registry.ts` — Vue entry added - `registry-primary-flag.ts` — `SupportedLanguages.Vue` added to `MIGRATED_LANGUAGES` (production default → registry-primary) - `vue-composition-api` — `<script setup lang="ts">`, defineProps / defineEmits macros, cross-file TS imports, computed refs - `vue-options-api` — `defineComponent({methods, computed, data})`, this-based method calls, imported utility calls - `vue-cross-file` — composable functions returning class instances, multi-level import chains, UserModel/PostModel method calls - `fieldFallbackOnMethodLookup: true` — Options API `this.X()` calls may not resolve through the type-binding layer (no formal class); fallback catches common patterns via declared field names. - `allowGlobalFreeCallFallback: false` — Vue uses explicit imports; workspace-wide unique-name fallback would produce spurious edges for built-ins (ref, reactive, defineProps, …). - Template expression calls intentionally out of scope: component- reference CALLS edges are already emitted by the legacy template extractor. Remaining template gaps tracked in abhigyanpatwari#1647. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vue): address P0/P1 review findings from abhigyanpatwari#1950 ## P0 #1 — missing scope-resolution hooks in vueProvider `pass3CollectImports` early-returns when `interpretImport` is undefined, producing zero IMPORTS and zero cross-file CALLS edges. Add the four hooks to `vueProvider` in `vue.ts`: - `interpretImport: interpretTsImport` - `interpretTypeBinding: interpretTsTypeBinding` - `bindingScopeFor: tsBindingScopeFor` - `importOwningScope: tsImportOwningScope` Also add `receiverBinding`, `mergeBindings`, `arityCompatibility`, and `resolveImportTarget` to complete the scope-resolution contract. ## P0 #2 — template-component CALLS dropped when Vue is registry-primary `isRegistryPrimary(Vue) → true` makes the main call-processor loop skip Vue files entirely, silencing the inline `vue-template-component` CALLS emitter at ≈L1506. Add a dedicated post-loop pass in `call-processor.ts` that emits template-component CALLS for Vue files whenever Vue is registry-primary. Update the stale `vue/index.ts` limitation comment to reflect the new emit site. ## P1 #3 — worker-mode double-extraction → zero captures In worker mode (≥15 files) the parse worker pre-extracts the `<script>` block and passes `scriptContent` as `sourceText`. `emitVueScopeCaptures` was calling `extractVueScript` a second time, getting null, and returning `[]`. Fix: if extraction returns null and the content has no SFC block- level markers (`<template`, `<style`), treat it as already-extracted script text and delegate directly to `emitTsScopeCaptures`. ## Test assertion strictness Replace all `toBeGreaterThanOrEqual(1)` assertions with exact `toBe(N)` counts. IMPORTS counts reflect per-symbol scope-based edges (value imports only; `import type` is not emitted as an IMPORTS edge). CALLS counts are 1 per single-call-site. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(vue): template-derived edges + pipeline benchmark (abhigyanpatwari#1950 review) Addresses the reviewer's request for template edge attribution and a performance benchmark. ## Template event-handler CALLS (`vue-template-callback`) Add `extractTemplateEventHandlers` to `vue-sfc-extractor.ts`. Extracts bare single-identifier handlers from `@event="methodName"` and `v-on:event="methodName"` attributes. Inline expressions with arguments or operators (`@click="toggle(item)"`) are intentionally excluded. Wire into the dedicated registry-primary Vue template pass in `call-processor.ts`. For each extracted handler name, `ctx.resolve` finds the in-file Function/Method node and emits a CALLS edge with `reason: 'vue-template-callback'`. ## Template attribute-binding ACCESSES (`vue-template-attribute`) Add `extractTemplateAttributeBindings` to `vue-sfc-extractor.ts`. Extracts bare single-identifier values from `:prop="varName"` and `v-bind:prop="varName"` bindings. Member-access (`:key="post.id"`) and literals are excluded by the identifier-boundary regex. Wire into the same template pass. For each extracted variable, `ctx.resolve` finds the in-file node and emits an ACCESSES edge with `reason: 'vue-template-attribute'`. ## `vue/index.ts` limitations comment Updated to accurately describe all three categories of template-derived edges and explicitly document the complex-expression exclusions. ## Tests Add 6 new assertions in `vue-scope.test.ts`: - `@click="handleSave"` → CALLS `handleSave` (UserProfile.vue) - `@select="onPostSelected"` → CALLS `onPostSelected` (App.vue composition) - `@keyup.enter="addTodo"` → CALLS `addTodo` (TodoList.vue) - `@loaded="onUserLoaded"` → CALLS `onUserLoaded` (App.vue cross-file) - `:userId="currentUserId"` → ACCESSES `currentUserId` (App.vue composition) - `:posts="allPosts"` → ACCESSES `allPosts` (App.vue composition) Add `vue` entry to `LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES` in `helpers.ts` documenting which assertions are registry-primary-only (IMPORTS cardinality, template-derived edges, `<script setup>` export). ## Benchmark Add `vue-pipeline-benchmark.test.ts` (gated by `GITNEXUS_BENCH=1`). Generates N-component synthetic repos (10 / 25 / 50 / 100) and asserts that wall-clock and node counts scale sub-quadratically with component count, guarding against O(n²) regressions in the template extraction or scope-resolution passes. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(vue): BINDS_EVENT_HANDLER/EMITS_EVENT edges via ScopeResolver hook Per maintainer feedback on PR abhigyanpatwari#1950: - Do not edit call-processor.ts (will be removed when all languages migrate) - Model Vue component-event system with dedicated edge types to avoid CALLS noise in deep component hierarchies (per contributor discussion) Changes: - gitnexus-shared: add BINDS_EVENT_HANDLER and EMITS_EVENT to RelationshipType - vue-sfc-extractor: add extractComponentEventBindings, extractNativeElementEventHandlers, and extractScriptEmitCalls - ScopeResolver contract: add optional emitPostResolutionEdges hook - run.ts: wire emitPostResolutionEdges after emitImportEdges - vue/scope-resolver: implement emitPostResolutionEdges emitting: 1. CALLS (vue-template-component) — PascalCase component File refs 2. CALLS (vue-template-callback) — @event on native HTML elements 3. BINDS_EVENT_HANDLER (vue-event: @name) — @event on component elements; source = handler fn in parent, target = child component File (not CALLS) 4. EMITS_EVENT (vue-emit: name) — emit() calls; self-loop on component File, joinable with BINDS_EVENT_HANDLER via Cypher for impact tracing 5. ACCESSES (vue-template-attribute) — :prop="var" bindings - call-processor.ts: revert dedicated Vue post-loop pass; moved to scope resolver - Tests and parity expected-failures updated accordingly Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vue): close review gaps in scope/parity extraction Resolve the new PR abhigyanpatwari#1950 review findings by widening Vue scope context to include TS/JS import closures, fixing BINDS_EVENT_HANDLER endpoint assertions, hardening emit/event extraction to avoid comment/property false positives, supporting kebab-case component tags, and ensuring parity runs include vue-scope suites. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vue): address second review round — regex safety, emit coverage, arch Closes items raised in the Jun 2 review comment on PR abhigyanpatwari#1950. Correctness fixes: - ReDoS mitigation: bound attribute-capture spans to [^>]{0,512}? in all three template tag regexes to prevent pathological backtracking. - Kebab-case misclassified as native: added (?![A-Za-z0-9-]) negative lookahead to NATIVE_TAG_RE so <post-list> is no longer split as native tag `post` with attrs `-list ...`. - Hyphenated event names dropped: widened TAG_EVENT_RE from [\w:.]+ to [\w:.-]+ so @user-loaded and @update:model-value are captured. - this.$emit silently dropped: collectBareEmitEventNames now allows this.$emit(...) by looking back past the '.' to verify preceding token is exactly `this`; socket.emit etc. remain blocked. - Event names with colon rejected: extended validator to accept update:modelValue and update:model-value patterns. Architecture fix: - Moved collectVueScopeFilePaths out of shared phase.ts into a new collectScopeContextPaths optional hook on ScopeResolver, keeping shared pipeline code language-agnostic. vueScopeResolver implements the hook. - Fixed memory leak: preExtractedByPath cleanup now iterates filePaths (all context files) not just primaryFilePaths (only .vue files). Cleanup: - Removed unused extractTemplateEventHandlers and duplicate EVENT_HANDLER_RE. - Fixed skipped comment numbers in emitPostResolutionEdges (1,2,4,5,6 -> 1-6). - Updated vue/index.ts: four categories -> five (added EMITS_EVENT). - Fixed gitnexus-shared EMITS_EVENT JSDoc to reflect File->File reality. Tests: 7 new unit tests covering hyphenated events, this.$emit, kebab-case native-tag exclusion, and update:modelValue event name validation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vue): eliminate double file-read and per-file template re-scans Two performance fixes from the self-review pass: 1. **No more double read of .vue files in phase.ts**: primary files were previously read once for `collectScopeContextPaths` (via `entryFileContents`) and again in the blanket `readFileContents(filePaths)` call. Now the primary-file map is passed directly and only the extra context files (TS/JS import closure) require a second I/O round-trip. 2. **Single template parse per .vue file in emitPostResolutionEdges**: previously each of the five extractor functions (components, native handlers, component event bindings, emit calls, attribute bindings) ran `TEMPLATE_RE.exec(content)` independently — five full-file scans per `.vue` file. Replaced with a new `extractVueTemplateEdgeData` batching helper that parses the template and script blocks once and feeds all five extractors from the pre-extracted content. emitPostResolutionEdges now calls a single function and destructures the results. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(parity): exclude TypeScript HOC/HOF/JSX scope-resolver tests from legacy DAG parity gate Three test files introduced in prior PRs exercise scope-resolver-only correctness wins: HOC-wrapped const declarations, HOF-callback caller attribution, and JSX-as-call CALLS edges. The parity runner's ${slug}-*.test.ts glob now picks them up, causing typescript [legacy] failures in CI. Fix: convert each file to use createResolverParityIt('typescript') and register all 26 legacy-failing test names in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.typescript with explanatory comments. Legacy mode: 11+11+4 tests skipped, zero failures. Registry-primary mode: all 37 tests pass as before. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(test): remove registry-primary-flag unit tests after migration complete All languages are now in MIGRATED_LANGUAGES; the per-language flip tests are no longer needed. Addresses PR abhigyanpatwari#1950 review feedback. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
henry201605
pushed a commit
that referenced
this pull request
Jun 8, 2026
…i#2024) * docs: add CODEOWNERS (#2) * docs: add CI badge to README * docs: add CODEOWNERS file --------- Co-authored-by: Typo Fix Bot <fix@example.com> * docs: add CI badge to README (#1) Co-authored-by: Typo Fix Bot <fix@example.com> * docs: clarify local development setup in CONTRIBUTING * Update CODEOWNERS --------- Co-authored-by: Typo Fix Bot <fix@example.com> Co-authored-by: Arvuno <arvuno@nous.local> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
henry201605
pushed a commit
that referenced
this pull request
Jun 8, 2026
…wari#1928) (abhigyanpatwari#2045) * fix(java): close parsing-layer coverage gaps F35/F38/F41 (abhigyanpatwari#1928) Registry-primary scope-resolution path (the live one post-abhigyanpatwari#942/abhigyanpatwari#943): - F35 [HIGH]: qualified / qualified-generic constructor calls. `new pkg.Foo()` parses as a `scoped_type_identifier` that the query bound only as `@reference.call.constructor.qualified` with no `@reference.name`, so the scope extractor fell back to the whole-expression anchor and the reference name became the raw `new pkg.Foo()` text (never resolved). Bind the simple -name tail (end-anchored last child) and add an arm for the previously uncaptured `new pkg.Box<String>()` (qualified + generic) shape. - F38 [MEDIUM]: `super(...)` / `this(...)` explicit constructor invocations, modeled as `explicit_constructor_invocation` and never matched by the scope query, dropped the chained-constructor CALLS edges. Synthesize them with the target resolved structurally (this -> enclosing type name; super -> superclass tail via the shared javaBaseLookupNameNode, skipping implicit Object) plus arity for overload disambiguation. - F41 [LOW]: interpretJavaTypeBinding stripped the qualifier before generics, so a qualified generic type arg (`Map<String, com.example.User>`) was cut inside the generic into `User>`. Strip generics first, then the qualifier; make the erasure fallback qualifier-tolerant. F36/F37 already landed upstream (abhigyanpatwari#1940/abhigyanpatwari#1956); F39/F40 are legacy-bank remnants that are no longer consumed (legacy @import skipped in parse-worker; legacy @call never read in parse-impl) so they are intentionally left untouched. Tests: low-level capture unit tests (constructor shapes incl. double-match guard; super/this/enum/implicit-Object), interpretJavaTypeBinding unit tests (qualified generic args + the corruption case), and end-to-end resolver tests with new fixtures asserting the CALLS edges resolve to the correct constructors. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scope-resolution): register Constructor overload keys so this()/super() chains don't self-loop (abhigyanpatwari#1928 F38 review) Review of abhigyanpatwari#2045 caught two gaps; both confirmed by reproduction. P2 — F38 this() emitted a self-loop. On the java-explicit-constructor fixture, Child(int){ this(); } produced CALLS Child()#0 -> Child()#0 instead of Child(int)#1 -> Child()#0. Root cause is the language-agnostic graph-bridge: the parse phase mints distinct Constructor nodes (Child#0, Child#1) carrying parameterTypes, but node-lookup.ts registered the parameter-types / shape overload keys only for Function/Method, never Constructor, so both ctors collapsed onto the first-wins qualified/simple key and the caller Child(int) resolved to Child#0 (the this() target). Extend the overload keys to Constructor in both node-lookup.ts (registration) and ids.ts (lookup) via a shared isOverloadableCallable predicate. Verified the edge now connects distinct nodes (Child#1 -> Child#0); super(1)->Base#1 still correct. No cross-language regressions (the 9 worker-path failures reproduce identically on clean HEAD). Also harden the integration test: it matched the this() edge on name only, which a self-loop satisfies; now assert the endpoints are DISTINCT constructors. P3 — F41 order-regression guard was inert (List<Map<String,User>> normalizes to List under both strip orders). Add List<com.x.Foo<String>> -> List, which is corrupted to Foo<String>> under the old order and only correct generics-first. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(java): update fingerprint and add notes for constructor query captures in baselines.json Updated the fingerprint for the Java section and added detailed notes regarding the enhancements in constructor query captures, including qualified and qualified-generic constructor queries. This change reflects ongoing improvements in the parsing layer coverage and fixture updates. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
henry201605
pushed a commit
that referenced
this pull request
Jun 9, 2026
…orts
Addresses all 4 inline review comments:
1. Rewrote spring.ts to use a single predicate-free Parser.Query
(same pattern as group-layer JAVA_ROUTE_ANNOTATION_PATTERNS).
Two-phase loop: first pass collects class prefixes by node.id,
second pass resolves method routes via findEnclosingClass.
No more manual DFS / recursion.
2-3. Moved inline import(...) type references in language-provider.ts
to proper top-level imports (Parser, ExtractedDecoratorRoute).
4. Covered by #1 — recursive helpers removed entirely.
Added 3 extra test cases: non-route named args filtering,
prefix isolation across mixed classes, line number accuracy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When the LadybugDB FTS extension cannot be loaded (e.g. macOS 12 where system
libc++lacksstd::to_chars(double), or container environments without the native extension),gitnexus analyze --embeddingsfails at 85% and aborts before embedding generation can run.This blocks the entire embedding pipeline even though FTS and embeddings are independent features.
Root Cause
createSearchFTSIndexes()inrunFullAnalysis()throws on failure, which is inconsistent with the MCP server's behavior:pool-adapter.tsalready treats FTS load failure as a graceful degradation (ftsLoaded = false, BM25 search degrades, other features continue).Fix
Wrap
createSearchFTSIndexes()in try/catch so that:--repair-ftsremains available for explicit FTS retryAffected Users
libc++missing__ZNSt3__18to_charsEPcS0_dTesting
Verified on macOS 12.2.1 (arm64):
analyze --embeddingsfails at 85% with FTS error